package com.seashop.tree.init;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.seashop.constant.RedisConstant;
import com.seashop.entity.MemberFinanceLog;
import com.seashop.entity.MemberRelation;
import com.seashop.redis.RedisOne;
import com.seashop.redis.RedisZero;
import com.seashop.service.IMemberFinanceLogService;
import com.seashop.service.IMemberRelationService;
import com.seashop.tree.FinancialComputingFactory;
import com.seashop.tree.IFinancialComputing;
import com.seashop.tree.UserNodeRedis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

/**
 * @author moredln
 * @desc
 * @date 11/25/20 12:17 AM
 */
@Component
@Slf4j
public class UserTree {

    @Autowired
    private IMemberRelationService memberRelationService;
    @Autowired
    private IMemberFinanceLogService memberFinanceLogService;
    @Autowired
    private FinancialComputingFactory financialComputingFactory;
    @Autowired
    private IFinancialComputing financialComputing;
    @Autowired
    private RedisOne redisOne;
    @Autowired
    private RedisZero redisZero;
    private static DateTimeFormatter USER_TREE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static DateTimeFormatter USER_TREE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    // member regulation version
    private static String CURRENT_MEMBER_RULE_VERSION = "1.0";

//    @PostConstruct
    public void init() {
        log.info("UserTree init start ...");
        // refresh redis (db1) when time diff is greater than 2 hour
        boolean isOtherRunning = redisOne.exists(RedisConstant.OTHER_NODE_REFRESHING_KEY);
        boolean isNeedRefresh = false;
        if (!isOtherRunning) {
            if (!redisOne.exists(RedisConstant.LAST_UPDATE_TIME_KEY)) {
                isNeedRefresh = true;
            } else {
                LocalDateTime lastUpdateTime = readTimeFromRedis(RedisConstant.LAST_UPDATE_TIME_KEY);
                if (lastUpdateTime != null) {
                    LocalDateTime curTime = LocalDateTime.now();
                    Duration diffDuration = Duration.between(lastUpdateTime, curTime);
                    if (diffDuration.toHours() > 2) {
                        isNeedRefresh = true;
                    }
                } else {
                    isNeedRefresh = true;
                }
            }
        }
        if (isNeedRefresh) {
            writeTime2Redis(RedisConstant.OTHER_NODE_REFRESHING_KEY, LocalDateTime.now());
            initTree();
            writeTime2Redis(RedisConstant.LAST_UPDATE_TIME_KEY, LocalDateTime.now());
        }
        if (redisOne.exists(RedisConstant.OTHER_NODE_REFRESHING_KEY)) {
            redisOne.delete(RedisConstant.OTHER_NODE_REFRESHING_KEY);
        }

        redisZero.set("user_tree_current_version", RedisConstant.CURRENT_MEMBER_RULE_VERSION);
    }

    private void writeTime2Redis(String key, LocalDateTime theTime) {
        redisOne.set(key, theTime.format(USER_TREE_TIME_FORMATTER));
    }

    private LocalDateTime readTimeFromRedis(String key) {
        if (!redisOne.exists(key)) {
            return null;
        }
        String lastTime = String.valueOf(redisOne.get(key));
        LocalDateTime ldt = LocalDateTime.parse(lastTime, USER_TREE_TIME_FORMATTER);
        return ldt;
    }

    private void initTree() {
        if (!redisOne.exists(RedisConstant.NO_PARENT_MEMBERS)) {
            List<Integer> emptyList = new ArrayList<>();
            redisOne.set(RedisConstant.NO_PARENT_MEMBERS, JSON.toJSONString(emptyList));
            log.info("Create empty noParentMembersRedis");
        }
        // load all users and relationships from DB
        List<MemberRelation> memberRelations = memberRelationService.list(new QueryWrapper<MemberRelation>().orderByAsc("create_time"));
        // memberRelations=memberRelations.subList(0, 100);
        // create empty node on redis
        for (MemberRelation mr : memberRelations) {
            if (redisOne.exists(mr.getUserId().toString())) {
                redisOne.delete(mr.getUserId().toString());
            }
            log.info("[initTree] addNewMember:" + mr.toString());
            UserNodeRedis userNodeRedis = new UserNodeRedis(mr);
            redisOne.set(mr.getUserId().toString(), userNodeRedis.toString());
        }

        int i = 1;
        int totalSize = memberRelations.size();
        for (MemberRelation mr : memberRelations) {
            log.info("[initTree] setParent " + i + "/" + totalSize + ":" + mr.toString());
            setUserParent(mr.getUserId(), mr.getParentId(), false);
            i += 1;
        }
    }

    public synchronized boolean setUserParent(Integer userId, Integer parentId, boolean isForceModifyParent) {
        // avoid self binding
        if (parentId.equals(userId)) {
            log.error("[setUserParent] user " + userId + " bind self!");
            return false;
        }

        // avoid close-loop
        if (isCloseLoop(userId, parentId)) {
            log.error("[setUserParent] user " + userId + " exists close-loop when set parent " + parentId);
            return false;
        }
        UserNodeRedis userNodeRedis = getUserNodeRedis(userId);
        if (userNodeRedis == null) {
            log.error("[setUserParent] user " + userId + " does not exists!");
            return false;
        }

        // add first generation members
        if (parentId.equals(-1)) {
            List<Integer> intList = getFirstGenerationMemberIdList();
            if (!intList.contains(userId)) {
                intList.add(userId);
                redisOne.set("noParentMembersRedis", JSON.toJSONString(intList));
            }
        }

        // note: support transfer sub-tree
        UserNodeRedis newParentNodeRedis = getUserNodeRedis(parentId);
        if (parentId > 0 && newParentNodeRedis == null) {
            log.warn("[setUserParent] parent " + parentId + " does not exists!");
            return false;
        }
        if (newParentNodeRedis != null && newParentNodeRedis.hasChild(userId)) {
            log.error("[setUserParent] parent " + parentId + " had contained this child " + userId);
            return false;
        }

        if (userNodeRedis.getParentId() != parentId) {
            if (!isForceModifyParent) {
                log.warn("[setUserParent] no permission for modifying " + userId + " parent from " + userNodeRedis.getParentId() + " to " + parentId);
                return false;
            }
            UserNodeRedis oldParentNodeRedis = getUserNodeRedis(userNodeRedis.getParentId());
            if (oldParentNodeRedis != null) {
                oldParentNodeRedis.removeChild(userNodeRedis);
                redisOne.set(String.valueOf(oldParentNodeRedis.getUserId()), oldParentNodeRedis.toString());
                // traverse old parent's ancestors
                UserNodeRedis curUserNodeRedis = oldParentNodeRedis;
                while (curUserNodeRedis.getParentId() > 0) {
                    UserNodeRedis ancestorNodeRedis = getUserNodeRedis(curUserNodeRedis.getParentId());
                    // remove self on indirect ancestors' teams
                    ancestorNodeRedis.removeDescendantFromAncestorTeam(userNodeRedis.getUserLevel());
                    // update sub-tree on old ancestors
                    ancestorNodeRedis.removeSubTreeTeamCounts(userNodeRedis);
                    redisOne.set(String.valueOf(ancestorNodeRedis.getUserId()), ancestorNodeRedis.toString());
                    curUserNodeRedis = ancestorNodeRedis;
                }
            }
            // remove from first generation members
            if (userNodeRedis.getParentId() == -1) {
                List<Integer> intList = getFirstGenerationMemberIdList();
                if (intList.contains(userId)) {
                    intList.remove(userId);
                    redisOne.set(RedisConstant.NO_PARENT_MEMBERS, JSON.toJSONString(intList));
                }
            }
        }

        if (parentId.equals(-1) || newParentNodeRedis == null) {
            return true;
        }

        userNodeRedis.setParentId(parentId);
        redisOne.set(userId.toString(), userNodeRedis.toString());
        newParentNodeRedis.addChild(userNodeRedis);
        redisOne.set(parentId.toString(), newParentNodeRedis.toString());

        // traverse new parent's ancestors
        UserNodeRedis curUserNodeRedis = newParentNodeRedis;
        while (curUserNodeRedis.getParentId() > 0) {
            UserNodeRedis ancestorNodeRedis = getUserNodeRedis(curUserNodeRedis.getParentId());
            // add self on indirect ancestors' teams
            ancestorNodeRedis.addDescendant2AncestorTeam(userNodeRedis.getUserLevel());
            // update sub-tree on new ancestors
            ancestorNodeRedis.addSubTreeTeamCounts(userNodeRedis);
            redisOne.set(String.valueOf(ancestorNodeRedis.getUserId()), ancestorNodeRedis.toString());
            curUserNodeRedis = ancestorNodeRedis;
        }

        return true;
    }

    private List<Integer> getFirstGenerationMemberIdList() {
        JSONArray valueJson = JSON.parseArray(redisOne.get("noParentMembersRedis").toString());
        List<Integer> intList = valueJson.toJavaList(Integer.class);
        return intList;
    }

    private UserNodeRedis getUserNodeRedis(Integer userId) {
        if (userId == null) {
            log.error("[getUserNodeRedis] userId (null) is invalid!");
            return null;
        }
        if (userId.intValue() < 0) {
            log.warn("[getUserNodeRedis] userId " + userId + " is invalid!");
            return null;
        }
        Object valueJson = redisOne.get(userId.toString());
        if (valueJson == null) {
            log.warn("[getUserNodeRedis] userId " + userId + " not exists!");
            return null;
        }

        UserNodeRedis userNodeRedis = new UserNodeRedis(valueJson.toString());
        if (userNodeRedis == null) {
            return null;
        }

        // check vip valid
        String vipExpireTime = userNodeRedis.getVipExpireTime();
        if (!vipExpireTime.isEmpty() && userNodeRedis.getUserLevel() == 2) {
            LocalDateTime expireTime = LocalDateTime.parse(vipExpireTime, USER_TREE_TIME_FORMATTER);
            LocalDateTime curTime = LocalDateTime.now();
            if (expireTime.compareTo(curTime) <= 0) {
                MemberFinanceLog memberFinanceLog = memberFinanceLogService.getOne(new QueryWrapper<MemberFinanceLog>().eq("user_id", userId).eq("behavior", "buyvip").orderByDesc("create_time"));
                if (memberFinanceLog != null) {
                    String countryCode = memberFinanceLog.getCountryCode();
                    degradeMember(userNodeRedis, countryCode, true);
                }
            }
        }

        return userNodeRedis;
    }

    public synchronized boolean degradeMember(UserNodeRedis userNodeRedis, String countryCode, boolean isBuyer) {
        IFinancialComputing iFinancialComputing = financialComputingFactory.getByCountry(countryCode, CURRENT_MEMBER_RULE_VERSION);
        if (userNodeRedis == null) {
            log.warn("[degradeMember] user does not exists!");
            return false;
        }
        // parent and ancestors degrade
        if (!iFinancialComputing.checkDegrade(userNodeRedis, isBuyer)) {
            return false;
        }
        int oldLevel = userNodeRedis.getUserLevel();
        userNodeRedis.degrade();
        // DB
        MemberRelation memberRelation = memberRelationService.memberDegrade(userNodeRedis.getUserId(), userNodeRedis.getUserLevel());
        userNodeRedis.setVipStatus(memberRelation.getVipStatus());

        int newLevel = userNodeRedis.getUserLevel();
        if (oldLevel == newLevel) {
            if (isBuyer) return false;
        } else {
            redisOne.set(String.valueOf(userNodeRedis.getUserId()), userNodeRedis.toString());
        }

        // upward parents
        UserNodeRedis parentNodeRedis = getUserNodeRedis(userNodeRedis.getParentId());
        if (parentNodeRedis == null) {
            return true;
        }

        // synchronize parent's child & team map
        parentNodeRedis.changeChildrenCount(oldLevel, newLevel);
        parentNodeRedis.changeTeamCount(oldLevel, newLevel);
        redisOne.set(String.valueOf(parentNodeRedis.getUserId()), parentNodeRedis.toString());

        UserNodeRedis curNodeRedis = parentNodeRedis;
        // synchronize ancestors' (exclude parent) team maps
        while (curNodeRedis.getParentId() > 0) {
            curNodeRedis = getUserNodeRedis(curNodeRedis.getParentId());
            curNodeRedis.changeTeamCount(oldLevel, newLevel);
            redisOne.set(String.valueOf(curNodeRedis.getUserId()), curNodeRedis.toString());
        }

        // upward recursive upgrade from parent
        degradeMember(parentNodeRedis, countryCode, false);

        return true;
    }

    private boolean isCloseLoop(Integer userId, Integer parentId) {
        if (parentId.equals(-1)) {
            return false;
        }

        if (getUserNodeRedis(parentId).getParentId() == -1) {
            return false;
        }

        List<Integer> ancestors = getAncestors(parentId);
        if (ancestors.size() <= 0) {
            return true;
        }
        for (Integer ancestorId : ancestors) {
            if (ancestorId.equals(userId)) {
                return true;
            }
        }
        return false;
    }

    // core algorithms
    private List<Integer> getAncestors(Integer userId) {
        List<Integer> ancestors = new ArrayList<>();
        UserNodeRedis userNodeRedis = getUserNodeRedis(userId);
        if (userNodeRedis == null) {
            log.warn("[getAncestors] user " + userId + " does not exists!");
            return ancestors;
        }
        while (userNodeRedis.getParentId() > 0) {
            Integer parentId = userNodeRedis.getParentId();
            if (parentId.equals(userId)) {
                log.error("[getAncestors] Exist close loop!");
                ancestors.clear();
                return ancestors;
            }
            ancestors.add(parentId);
            userNodeRedis = getUserNodeRedis(parentId);
            if (userNodeRedis == null) {
                log.warn("[getAncestors] parent " + parentId + " does not exists!");
            }
        }
        return ancestors;
    }


}
